diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index 0737e8e8df..6c8b5834fb 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -43,6 +43,7 @@ public EntitiesContext(string connectionString, bool readOnly) public IDbSet Scopes { get; set; } public IDbSet Users { get; set; } public IDbSet UserSecurityPolicies { get; set; } + public IDbSet ReservedNamespaces { get; set; } IDbSet IEntitiesContext.Set() { @@ -127,6 +128,23 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .HasForeignKey(p => p.UserKey) .WillCascadeOnDelete(true); + modelBuilder.Entity() + .HasKey(p => p.Key); + + modelBuilder.Entity() + .HasMany(rn => rn.PackageRegistrations) + .WithMany(pr => pr.ReservedNamespaces) + .Map(prrn => prrn.ToTable("ReservedNamespaceRegistrations") + .MapLeftKey("ReservedNamespaceKey") + .MapRightKey("PackageRegistrationKey")); + + modelBuilder.Entity() + .HasMany(pr => pr.Owners) + .WithMany(u => u.ReservedNamespaces) + .Map(c => c.ToTable("ReservedNamespaceOwners") + .MapLeftKey("ReservedNamespaceKey") + .MapRightKey("UserKey")); + modelBuilder.Entity() .HasKey(p => p.Key); diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index e24dd4089b..6f85ac73e9 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -15,6 +15,7 @@ public interface IEntitiesContext IDbSet Scopes { get; set; } IDbSet Users { get; set; } IDbSet UserSecurityPolicies { get; set; } + IDbSet ReservedNamespaces { get; set; } Task SaveChangesAsync(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")] diff --git a/src/NuGetGallery.Core/Entities/PackageRegistration.cs b/src/NuGetGallery.Core/Entities/PackageRegistration.cs index fd14509d42..a68e7557f1 100644 --- a/src/NuGetGallery.Core/Entities/PackageRegistration.cs +++ b/src/NuGetGallery.Core/Entities/PackageRegistration.cs @@ -13,6 +13,7 @@ public PackageRegistration() { Owners = new HashSet(); Packages = new HashSet(); + ReservedNamespaces = new HashSet(); } [StringLength(CoreConstants.MaxPackageIdLength)] @@ -20,8 +21,13 @@ public PackageRegistration() public string Id { get; set; } public int DownloadCount { get; set; } + + public bool IsVerified { get; set; } + public virtual ICollection Owners { get; set; } public virtual ICollection Packages { get; set; } + public virtual ICollection ReservedNamespaces { get; set; } + public int Key { get; set; } } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/Entities/ReservedNamespace.cs b/src/NuGetGallery.Core/Entities/ReservedNamespace.cs new file mode 100644 index 0000000000..c034efc8d4 --- /dev/null +++ b/src/NuGetGallery.Core/Entities/ReservedNamespace.cs @@ -0,0 +1,39 @@ +// 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; + +namespace NuGetGallery +{ + public class ReservedNamespace : IEntity + { + public ReservedNamespace() + : this(value: null, isSharedNamespace: false, isPrefix: false) + { + } + + public ReservedNamespace(string value, bool isSharedNamespace, bool isPrefix) + { + Value = value; + IsSharedNamespace = isSharedNamespace; + IsPrefix = isPrefix; + PackageRegistrations = new HashSet(); + Owners = new HashSet(); + } + + [StringLength(CoreConstants.MaxPackageIdLength)] + [Required] + public string Value { get; set; } + + public bool IsSharedNamespace { get; set; } + + public bool IsPrefix { get; set; } + + public virtual ICollection PackageRegistrations { get; set; } + public virtual ICollection Owners { get; set; } + + [Key] + public int Key { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Entities/SuspendDbExecutionStrategy.cs b/src/NuGetGallery.Core/Entities/SuspendDbExecutionStrategy.cs new file mode 100644 index 0000000000..5542cbed79 --- /dev/null +++ b/src/NuGetGallery.Core/Entities/SuspendDbExecutionStrategy.cs @@ -0,0 +1,23 @@ +// 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; + +namespace NuGetGallery +{ + /// + /// Define the execution strategy for the EntitiesConfiguration for connection resiliency and retries + /// + public class SuspendDbExecutionStrategy : IDisposable + { + public SuspendDbExecutionStrategy() + { + EntitiesConfiguration.SuspendExecutionStrategy = true; + } + + public void Dispose() + { + EntitiesConfiguration.SuspendExecutionStrategy = false; + } + } +} diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index 37886a8cf3..fd80103805 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -19,6 +19,7 @@ public User(string username) { Credentials = new List(); SecurityPolicies = new List(); + ReservedNamespaces = new HashSet(); Roles = new List(); Username = username; } @@ -36,8 +37,11 @@ public User(string username) public string Username { get; set; } public virtual ICollection Roles { get; set; } + public bool EmailAllowed { get; set; } + public virtual ICollection ReservedNamespaces { get; set; } + [DefaultValue(true)] public bool NotifyPackagePushed { get; set; } diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index a27dd1eb62..cf9f74d98f 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -178,11 +178,13 @@ + + diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index a0ef23e9c3..0fd606311b 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -89,6 +89,11 @@ protected override void Load(ContainerBuilder builder) .As>() .InstancePerLifetimeScope(); + builder.RegisterType>() + .AsSelf() + .As>() + .InstancePerLifetimeScope(); + builder.RegisterType>() .AsSelf() .As>() @@ -186,6 +191,11 @@ protected override void Load(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); + builder.RegisterType() + .AsSelf() + .As() + .InstancePerLifetimeScope(); + builder.RegisterType() .SingleInstance(); diff --git a/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.Designer.cs b/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.Designer.cs new file mode 100644 index 0000000000..8e9d2fc473 --- /dev/null +++ b/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class PrefixReservation : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(PrefixReservation)); + + string IMigrationMetadata.Id + { + get { return "201708241907124_PrefixReservation"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.cs b/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.cs new file mode 100644 index 0000000000..92dd7d0e29 --- /dev/null +++ b/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.cs @@ -0,0 +1,66 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class PrefixReservation : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.ReservedNamespaces", + c => new + { + Key = c.Int(nullable: false, identity: true), + Value = c.String(nullable: false, maxLength: 128), + IsSharedNamespace = c.Boolean(nullable: false), + IsPrefix = c.Boolean(nullable: false), + }) + .PrimaryKey(t => t.Key); + + CreateTable( + "dbo.ReservedNamespaceOwners", + c => new + { + ReservedNamespaceKey = c.Int(nullable: false), + UserKey = c.Int(nullable: false), + }) + .PrimaryKey(t => new { t.ReservedNamespaceKey, t.UserKey }) + .ForeignKey("dbo.ReservedNamespaces", t => t.ReservedNamespaceKey, cascadeDelete: true) + .ForeignKey("dbo.Users", t => t.UserKey, cascadeDelete: true) + .Index(t => t.ReservedNamespaceKey) + .Index(t => t.UserKey); + + CreateTable( + "dbo.ReservedNamespaceRegistrations", + c => new + { + ReservedNamespaceKey = c.Int(nullable: false), + PackageRegistrationKey = c.Int(nullable: false), + }) + .PrimaryKey(t => new { t.ReservedNamespaceKey, t.PackageRegistrationKey }) + .ForeignKey("dbo.ReservedNamespaces", t => t.ReservedNamespaceKey, cascadeDelete: true) + .ForeignKey("dbo.PackageRegistrations", t => t.PackageRegistrationKey, cascadeDelete: true) + .Index(t => t.ReservedNamespaceKey) + .Index(t => t.PackageRegistrationKey); + + AddColumn("dbo.PackageRegistrations", "IsVerified", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropForeignKey("dbo.ReservedNamespaceRegistrations", "PackageRegistrationKey", "dbo.PackageRegistrations"); + DropForeignKey("dbo.ReservedNamespaceRegistrations", "ReservedNamespaceKey", "dbo.ReservedNamespaces"); + DropForeignKey("dbo.ReservedNamespaceOwners", "UserKey", "dbo.Users"); + DropForeignKey("dbo.ReservedNamespaceOwners", "ReservedNamespaceKey", "dbo.ReservedNamespaces"); + DropIndex("dbo.ReservedNamespaceRegistrations", new[] { "PackageRegistrationKey" }); + DropIndex("dbo.ReservedNamespaceRegistrations", new[] { "ReservedNamespaceKey" }); + DropIndex("dbo.ReservedNamespaceOwners", new[] { "UserKey" }); + DropIndex("dbo.ReservedNamespaceOwners", new[] { "ReservedNamespaceKey" }); + DropColumn("dbo.PackageRegistrations", "IsVerified"); + DropTable("dbo.ReservedNamespaceRegistrations"); + DropTable("dbo.ReservedNamespaceOwners"); + DropTable("dbo.ReservedNamespaces"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.resx b/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.resx new file mode 100644 index 0000000000..92152e09ba --- /dev/null +++ b/src/NuGetGallery/Migrations/201708241907124_PrefixReservation.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 69ef35a345..f6bea51da4 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -785,6 +785,10 @@ 201706262349176_AddRepositoryURL_ReadMe.cs + + + 201708241907124_PrefixReservation.cs + @@ -801,6 +805,8 @@ + + @@ -1777,6 +1783,9 @@ 201706262349176_AddRepositoryURL_ReadMe.cs + + 201708241907124_PrefixReservation.cs + diff --git a/src/NuGetGallery/Services/IPackageService.cs b/src/NuGetGallery/Services/IPackageService.cs index 3abbc2ae11..97a381a32d 100644 --- a/src/NuGetGallery/Services/IPackageService.cs +++ b/src/NuGetGallery/Services/IPackageService.cs @@ -84,5 +84,7 @@ public interface IPackageService void EnsureValid(PackageArchiveReader packageArchiveReader); Task IncrementDownloadCountAsync(string id, string version, bool commitChanges = true); + + Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection package, bool isVerified); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/IReservedNamespaceService.cs b/src/NuGetGallery/Services/IReservedNamespaceService.cs new file mode 100644 index 0000000000..cab511f568 --- /dev/null +++ b/src/NuGetGallery/Services/IReservedNamespaceService.cs @@ -0,0 +1,75 @@ +// 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.Threading.Tasks; + +namespace NuGetGallery +{ + public interface IReservedNamespaceService + { + /// + /// Create a new namespace with the given prefix + /// + /// The reserved namespace to be created + /// Awaitable Task + Task AddReservedNamespaceAsync(ReservedNamespace prefix); + + /// + /// Deallocate the reserved namespace with the given prefix, also removes + /// the verified property on all the package registrations which match + /// this reserved namespace only. + /// + /// The reserved namespace to be deleted + /// Awaitable Task + Task DeleteReservedNamespaceAsync(string prefix); + + /// + /// Adds the specified user as an owner to the reserved namespace. + /// Also, all the package registrations owned by this user which match the + /// specified namespace will be marked as verified. + /// + /// The reserved namespace to modify + /// The user who gets ownership of the namespace + /// Awaitable Task + Task AddOwnerToReservedNamespaceAsync(string prefix, string username); + + /// + /// Remove the specified user as an owner from the reserved namespace. + /// Also, all the package registrations owned by this user which match the + /// specified namespace only will be marked as unverifed. + /// + /// The reserved namespace to modify + /// The user to remove the ownership for the namespace + /// Awaitable Task + Task DeleteOwnerFromReservedNamespaceAsync(string prefix, string username); + + /// + /// Retrieves the first reserved namespace which matches the given prefix. + /// + /// The prefix to lookup + /// Reserved namespace matching the prefix + ReservedNamespace FindReservedNamespaceForPrefix(string prefix); + + /// + /// Retrieves all the reserved namespaces which matches the given prefix. + /// + /// The prefix to lookup + /// The list of reserved namespaces matching the prefix + IReadOnlyCollection FindAllReservedNamespacesForPrefix(string prefix, bool getExactMatches); + + /// + /// Retrieves all the reserved namespaces which matches the given list of prefixes. + /// + /// The list of prefixes to lookup + /// The list of reserved namespaces matching the prefixes + IReadOnlyCollection FindReservedNamespacesForPrefixList(IReadOnlyCollection prefixList); + + /// + /// Retrieves all the reserved namespaces which match the given id + /// + /// The package id to lookup + /// The list of reserved namespaces which are prefixes for the given id + IReadOnlyCollection GetReservedNamespacesForId(string id); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackageDeleteService.cs b/src/NuGetGallery/Services/PackageDeleteService.cs index 8792bf1f9a..9142e0bb50 100644 --- a/src/NuGetGallery/Services/PackageDeleteService.cs +++ b/src/NuGetGallery/Services/PackageDeleteService.cs @@ -56,7 +56,7 @@ public PackageDeleteService( public async Task SoftDeletePackagesAsync(IEnumerable packages, User deletedBy, string reason, string signature) { - EntitiesConfiguration.SuspendExecutionStrategy = true; + using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { // Increase command timeout @@ -100,7 +100,6 @@ public async Task SoftDeletePackagesAsync(IEnumerable packages, User de await _packageDeletesRepository.CommitChangesAsync(); transaction.Commit(); } - EntitiesConfiguration.SuspendExecutionStrategy = false; // Force refresh the index @@ -109,7 +108,7 @@ public async Task SoftDeletePackagesAsync(IEnumerable packages, User de public async Task HardDeletePackagesAsync(IEnumerable packages, User deletedBy, string reason, string signature, bool deleteEmptyPackageRegistration) { - EntitiesConfiguration.SuspendExecutionStrategy = true; + using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { // Increase command timeout @@ -156,7 +155,6 @@ await ExecuteSqlCommandAsync(_entitiesContext.GetDatabase(), // Commit transaction transaction.Commit(); } - EntitiesConfiguration.SuspendExecutionStrategy = false; // Force refresh the index UpdateSearchIndex(); diff --git a/src/NuGetGallery/Services/PackageService.cs b/src/NuGetGallery/Services/PackageService.cs index 14adb26fb2..d571fd0e1d 100644 --- a/src/NuGetGallery/Services/PackageService.cs +++ b/src/NuGetGallery/Services/PackageService.cs @@ -954,5 +954,20 @@ public async Task IncrementDownloadCountAsync(string id, string version, bool co } } } + + public virtual async Task UpdatePackageVerifiedStatusAsync(IReadOnlyCollection packageRegistrationList, bool isVerified) + { + var allPackageRegistrations = _packageRegistrationRepository.GetAll(); + var packageRegistrationsToUpdate = allPackageRegistrations + .Where(pr => packageRegistrationList.Any(prl => prl.Id == pr.Id)) + .ToList(); + + if (packageRegistrationsToUpdate.Count > 0) + { + packageRegistrationsToUpdate + .ForEach(pru => pru.IsVerified = isVerified); + await _packageRegistrationRepository.CommitChangesAsync(); + } + } } } diff --git a/src/NuGetGallery/Services/ReservedNamespaceService.cs b/src/NuGetGallery/Services/ReservedNamespaceService.cs new file mode 100644 index 0000000000..5970bb3286 --- /dev/null +++ b/src/NuGetGallery/Services/ReservedNamespaceService.cs @@ -0,0 +1,263 @@ +// 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 NuGet.Packaging; +using NuGetGallery.Auditing; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public class ReservedNamespaceService : IReservedNamespaceService + { + private static readonly Regex NamespaceRegex = new Regex(@"^\w+([_.-]\w+)*[.]?$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + public IEntitiesContext EntitiesContext { get; protected set; } + public IEntityRepository ReservedNamespaceRepository { get; protected set; } + public IUserService UserService { get; protected set; } + public IPackageService PackageService { get; protected set; } + public IAuditingService AuditingService { get; protected set; } + + protected ReservedNamespaceService() { } + + public ReservedNamespaceService( + IEntitiesContext entitiesContext, + IEntityRepository reservedNamespaceRepository, + IUserService userService, + IPackageService packageService, + IAuditingService auditing) + : this() + { + EntitiesContext = entitiesContext; + ReservedNamespaceRepository = reservedNamespaceRepository; + UserService = userService; + PackageService = packageService; + AuditingService = auditing; + } + + public async Task AddReservedNamespaceAsync(ReservedNamespace newNamespace) + { + if (newNamespace == null) + { + throw new ArgumentNullException(nameof(newNamespace)); + } + + ValidateNamespace(newNamespace.Value); + + var matchingReservedNamespaces = FindAllReservedNamespacesForPrefix(prefix: newNamespace.Value, getExactMatches: !newNamespace.IsPrefix); + if (matchingReservedNamespaces.Any()) + { + throw new InvalidOperationException(Strings.ReservedNamespace_NamespaceNotAvailable); + } + + ReservedNamespaceRepository.InsertOnCommit(newNamespace); + await ReservedNamespaceRepository.CommitChangesAsync(); + } + + public async Task DeleteReservedNamespaceAsync(string existingNamespace) + { + if (string.IsNullOrWhiteSpace(existingNamespace)) + { + throw new ArgumentException(Strings.ReservedNamespace_InvalidNamespace); + } + + using (var strategy = new SuspendDbExecutionStrategy()) + using (var transaction = EntitiesContext.GetDatabase().BeginTransaction()) + { + var namespaceToDelete = FindReservedNamespaceForPrefix(existingNamespace) + ?? throw new InvalidOperationException(string.Format( + CultureInfo.CurrentCulture, Strings.ReservedNamespace_NamespaceNotFound, existingNamespace)); + + // Delete verified flags on corresponding packages for this prefix if it is the only prefix matching the + // package registration. + if (!namespaceToDelete.IsSharedNamespace) + { + var packageRegistrationsToMarkUnverified = namespaceToDelete + .PackageRegistrations + .Where(pr => pr.ReservedNamespaces.Count() == 1) + .ToList(); + + if (packageRegistrationsToMarkUnverified.Any()) + { + await PackageService.UpdatePackageVerifiedStatusAsync(packageRegistrationsToMarkUnverified, isVerified: false); + } + } + + ReservedNamespaceRepository.DeleteOnCommit(namespaceToDelete); + await ReservedNamespaceRepository.CommitChangesAsync(); + + transaction.Commit(); + } + } + + public async Task AddOwnerToReservedNamespaceAsync(string prefix, string username) + { + if (string.IsNullOrWhiteSpace(prefix)) + { + throw new ArgumentException(Strings.ReservedNamespace_InvalidNamespace); + } + + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException(Strings.ReservedNamespace_InvalidUsername); + } + + using (var strategy = new SuspendDbExecutionStrategy()) + using (var transaction = EntitiesContext.GetDatabase().BeginTransaction()) + { + var namespaceToModify = FindReservedNamespaceForPrefix(prefix) + ?? throw new InvalidOperationException(string.Format( + CultureInfo.CurrentCulture, Strings.ReservedNamespace_NamespaceNotFound, prefix)); + + var userToAdd = UserService.FindByUsername(username) + ?? throw new InvalidOperationException(string.Format( + CultureInfo.CurrentCulture, Strings.ReservedNamespace_UserNotFound, username)); + + // Mark all packages owned by this user that start with the given namespace as verified. + var allPackageRegistrationsForUser = PackageService.FindPackageRegistrationsByOwner(userToAdd); + var packageRegistrationsMatchingNamespace = allPackageRegistrationsForUser + .Where(pr => pr.Id.StartsWith(namespaceToModify.Value, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (packageRegistrationsMatchingNamespace.Any()) + { + packageRegistrationsMatchingNamespace + .ForEach(pr => namespaceToModify.PackageRegistrations.Add(pr)); + + await PackageService.UpdatePackageVerifiedStatusAsync(packageRegistrationsMatchingNamespace, isVerified: true); + } + + namespaceToModify.Owners.Add(userToAdd); + await ReservedNamespaceRepository.CommitChangesAsync(); + + transaction.Commit(); + } + } + + public async Task DeleteOwnerFromReservedNamespaceAsync(string prefix, string username) + { + if (string.IsNullOrWhiteSpace(prefix)) + { + throw new ArgumentException(Strings.ReservedNamespace_InvalidNamespace); + } + + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException(Strings.ReservedNamespace_InvalidUsername); + } + + using (var strategy = new SuspendDbExecutionStrategy()) + using (var transaction = EntitiesContext.GetDatabase().BeginTransaction()) + { + var namespaceToModify = FindReservedNamespaceForPrefix(prefix) + ?? throw new InvalidOperationException(string.Format( + CultureInfo.CurrentCulture, Strings.ReservedNamespace_NamespaceNotFound, prefix)); + + var userToRemove = UserService.FindByUsername(username) + ?? throw new InvalidOperationException(string.Format( + CultureInfo.CurrentCulture, Strings.ReservedNamespace_UserNotFound, username)); + + if (!namespaceToModify.Owners.Contains(userToRemove)) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ReservedNamespace_UserNotAnOwner, username)); + } + + var packagesOwnedByUserMatchingPrefix = namespaceToModify + .PackageRegistrations + .Where(pr => pr + .Owners + .Any(pro => pro.Username == userToRemove.Username)) + .ToList(); + + // Remove verified mark for package registrations if the user to be removed is the only prefix owner + // for the given package registration. + var packageRegistrationsToMarkUnverified = packagesOwnedByUserMatchingPrefix + .Where(pr => pr.Owners.Intersect(namespaceToModify.Owners).Count() == 1) + .ToList(); + + if (packageRegistrationsToMarkUnverified.Any()) + { + packageRegistrationsToMarkUnverified + .ForEach(pr => namespaceToModify.PackageRegistrations.Remove(pr)); + + await PackageService.UpdatePackageVerifiedStatusAsync(packageRegistrationsToMarkUnverified, isVerified: false); + } + + namespaceToModify.Owners.Remove(userToRemove); + await ReservedNamespaceRepository.CommitChangesAsync(); + + transaction.Commit(); + } + } + + public ReservedNamespace FindReservedNamespaceForPrefix(string prefix) + { + return (from request in ReservedNamespaceRepository.GetAll() + where request.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase) + select request).FirstOrDefault(); + } + + public IReadOnlyCollection FindAllReservedNamespacesForPrefix(string prefix, bool getExactMatches) + { + Expression> prefixMatch; + if (getExactMatches) + { + prefixMatch = dbPrefix => dbPrefix.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase); + } + else + { + prefixMatch = dbPrefix => dbPrefix.Value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); + } + + return ReservedNamespaceRepository.GetAll() + .Where(prefixMatch) + .ToList(); + } + + public IReadOnlyCollection FindReservedNamespacesForPrefixList(IReadOnlyCollection prefixList) + { + return (from dbPrefix in ReservedNamespaceRepository.GetAll() + join queryPrefix in prefixList + on dbPrefix.Value equals queryPrefix + select dbPrefix).ToList(); + } + + public IReadOnlyCollection GetReservedNamespacesForId(string id) + { + return (from request in ReservedNamespaceRepository.GetAll() + where id.StartsWith(request.Value) + select request).ToList(); + } + + public static void ValidateNamespace(string value) + { + // Same restrictions as that of NuGetGallery.Core.Packaging.PackageIdValidator except for the regex change, a namespace could end in a '.' + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(Strings.ReservedNamespace_InvalidNamespace); + } + + if (value.Length > CoreConstants.MaxPackageIdLength) + { + throw new ArgumentException(string.Format( + CultureInfo.CurrentCulture, + Strings.ReservedNamespace_NamespaceExceedsLength, + CoreConstants.MaxPackageIdLength)); + } + + if (!NamespaceRegex.IsMatch(value)) + { + throw new ArgumentException(string.Format( + CultureInfo.CurrentCulture, + Strings.ReservedNamespace_InvalidCharactersInNamespace, + value)); + } + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 9d73a051ce..02a6c170cf 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -881,6 +881,78 @@ public static string PasswordSet { } } + /// + /// Looks up a localized string similar to The namespace '{0}' contains invalid characters. Examples of valid namespaces include 'MyNamespace' and 'MyNamespace.'.. + /// + public static string ReservedNamespace_InvalidCharactersInNamespace { + get { + return ResourceManager.GetString("ReservedNamespace_InvalidCharactersInNamespace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid namespace specified. + /// + public static string ReservedNamespace_InvalidNamespace { + get { + return ResourceManager.GetString("ReservedNamespace_InvalidNamespace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid or null username specified.. + /// + public static string ReservedNamespace_InvalidUsername { + get { + return ResourceManager.GetString("ReservedNamespace_InvalidUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Namespace must not exceed {0} characters.. + /// + public static string ReservedNamespace_NamespaceExceedsLength { + get { + return ResourceManager.GetString("ReservedNamespace_NamespaceExceedsLength", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified namespace is already reserved or is a more liberal namespace.. + /// + public static string ReservedNamespace_NamespaceNotAvailable { + get { + return ResourceManager.GetString("ReservedNamespace_NamespaceNotAvailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Namespace '{0}' not found.. + /// + public static string ReservedNamespace_NamespaceNotFound { + get { + return ResourceManager.GetString("ReservedNamespace_NamespaceNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User '{0}' is not an owner of the specified namespace. + /// + public static string ReservedNamespace_UserNotAnOwner { + get { + return ResourceManager.GetString("ReservedNamespace_UserNotAnOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User not found with username '{0}'. + /// + public static string ReservedNamespace_UserNotFound { + get { + return ResourceManager.GetString("ReservedNamespace_UserNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to All. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index f2f197a42f..dcc168cdbf 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -537,4 +537,28 @@ For more information, please contact '{2}'. This package will only be available to download with SemVer 2.0.0 compatible NuGet clients, such as Visual Studio 2017 (version 15.3) and above or NuGet client 4.3 and above. For more information, see https://go.microsoft.com/fwlink/?linkid=852248. + + The namespace '{0}' contains invalid characters. Examples of valid namespaces include 'MyNamespace' and 'MyNamespace.'. + + + Invalid namespace specified + + + Invalid or null username specified. + + + Namespace must not exceed {0} characters. + + + The specified namespace is already reserved or is a more liberal namespace. + + + Namespace '{0}' not found. + + + User '{0}' is not an owner of the specified namespace + + + User not found with username '{0}' + \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj index fb2bfa44d4..dc98320067 100644 --- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj +++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj @@ -430,6 +430,7 @@ + @@ -495,6 +496,7 @@ + diff --git a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs index 368dfa0162..cbdb09d675 100644 --- a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs @@ -13,13 +13,12 @@ using NuGetGallery.Framework; using NuGetGallery.Packaging; using Xunit; +using NuGetGallery.TestUtils; namespace NuGetGallery { public class ReflowPackageServiceFacts { - private static readonly string _packageHashForTests = "NzMzMS1QNENLNEczSDQ1SA=="; - private static ReflowPackageService CreateService( Mock entitiesContext = null, Mock packageService = null, @@ -54,7 +53,7 @@ public class TheReflowAsyncMethod public async Task ReturnsNullWhenPackageNotFound() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var packageService = SetupPackageService(package); var entitiesContext = SetupEntitiesContext(); @@ -76,7 +75,7 @@ public async Task ReturnsNullWhenPackageNotFound() public async Task RetrievesOriginalPackageBinary() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var packageService = SetupPackageService(package); var entitiesContext = SetupEntitiesContext(); @@ -98,7 +97,7 @@ public async Task RetrievesOriginalPackageBinary() public async Task RetrievesOriginalPackageMetadata() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var packageService = SetupPackageService(package); var entitiesContext = SetupEntitiesContext(); @@ -120,7 +119,7 @@ public async Task RetrievesOriginalPackageMetadata() public async Task RemovesOriginalFrameworks_Authors_Dependencies() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var packageService = SetupPackageService(package); var entitiesContext = SetupEntitiesContext(); @@ -142,7 +141,7 @@ public async Task RemovesOriginalFrameworks_Authors_Dependencies() public async Task UpdatesPackageMetadata() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var packageService = SetupPackageService(package); var entitiesContext = SetupEntitiesContext(); @@ -196,7 +195,7 @@ public async Task UpdatesPackageMetadata() public async Task UpdatesPackageLastEdited() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var lastEdited = package.LastEdited; var packageService = SetupPackageService(package); @@ -221,7 +220,7 @@ public async Task UpdatesPackageLastEdited() public async Task DoesNotUpdatePackageListed(bool listed) { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); package.Listed = listed; var packageService = SetupPackageService(package); @@ -244,7 +243,7 @@ public async Task DoesNotUpdatePackageListed(bool listed) public async Task CallsUpdateIsLatestAsync() { // Arrange - var package = CreateTestPackage(); + var package = PackageServiceUtility.CreateTestPackage(); var packageService = SetupPackageService(package); var entitiesContext = SetupEntitiesContext(); @@ -263,42 +262,6 @@ public async Task CallsUpdateIsLatestAsync() } } - private static Package CreateTestPackage() - { - var packageRegistration = new PackageRegistration(); - packageRegistration.Id = "test"; - - var framework = new PackageFramework(); - var author = new PackageAuthor { Name = "maarten" }; - var dependency = new PackageDependency { Id = "other", VersionSpec = "1.0.0" }; - - var package = new Package - { - Key = 123, - PackageRegistration = packageRegistration, - Version = "1.0.0", - Hash = _packageHashForTests, - SupportedFrameworks = new List - { - framework - }, - FlattenedAuthors = "maarten", - Authors = new List - { - author - }, - Dependencies = new List - { - dependency - }, - User = new User("test") - }; - - packageRegistration.Packages.Add(package); - - return package; - } - private static Mock SetupPackageService(Package package) { var packageRegistrationRepository = new Mock>(); diff --git a/tests/NuGetGallery.Facts/Services/ReservedNamespaceServiceFacts.cs b/tests/NuGetGallery.Facts/Services/ReservedNamespaceServiceFacts.cs new file mode 100644 index 0000000000..c0994ea6d3 --- /dev/null +++ b/tests/NuGetGallery.Facts/Services/ReservedNamespaceServiceFacts.cs @@ -0,0 +1,569 @@ +// 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 Moq; +using NuGetGallery.TestUtils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace NuGetGallery.Services +{ + public class ReservedNamespaceServiceFacts + { + public class TheAddReservedNamespaceAsyncMethod + { + [Theory] + [InlineData("NewNamespace", false, true)] + [InlineData("NewNamespace.", true, true)] + [InlineData("New.Namespace", false, false)] + [InlineData("New.Namespace.Exact", true, false)] + public async Task NewNamespaceIsReservedCorrectly(string value, bool isShared, bool isPrefix) + { + var newNamespace = new ReservedNamespace(value, isSharedNamespace: isShared, isPrefix: isPrefix); + + var service = new TestableReservedNamespaceService(); + await service.AddReservedNamespaceAsync(newNamespace); + + service.MockReservedNamespaceRepository.Verify( + x => x.InsertOnCommit( + It.Is( + rn => rn.Value == newNamespace.Value + && rn.IsPrefix == newNamespace.IsPrefix + && rn.IsSharedNamespace == newNamespace.IsSharedNamespace))); + + service.MockReservedNamespaceRepository.Verify(x => x.CommitChangesAsync()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task EmptyOrNullNamespaceThrowsException(string value) + { + var service = new TestableReservedNamespaceService(); + var addNamespace = new ReservedNamespace(value, isSharedNamespace: false, isPrefix: true); + await Assert.ThrowsAsync(async () => await service.AddReservedNamespaceAsync(addNamespace)); + } + + [Theory] + [InlineData("LooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongNaaaaaaaaaaaaaaaaaaaaaaaaaaaamespace")] + [InlineData("@InvalidNamespace")] + [InlineData("Invalid.Ch@rac#ters$-In(Name)space")] + public async Task InvalidNamespaceThrowsException(string value) + { + var service = new TestableReservedNamespaceService(); + var addNamespace = new ReservedNamespace(value, isSharedNamespace: false, isPrefix: true); + await Assert.ThrowsAsync(async () => await service.AddReservedNamespaceAsync(addNamespace)); + } + + [Fact] + public async Task ExistingNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var newNamespace = testNamespaces.FirstOrDefault(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await Assert.ThrowsAsync(async () => await service.AddReservedNamespaceAsync(newNamespace)); + } + + [Fact] + public async Task ExistingNamespaceWithDifferentPrefixStateThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var newNamespace = testNamespaces.FirstOrDefault(x => x.Value == "jquery"); + newNamespace.IsPrefix = !newNamespace.IsPrefix; + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await Assert.ThrowsAsync(async () => await service.AddReservedNamespaceAsync(newNamespace)); + } + + [Fact] + public async Task LiberalNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + // test case has a namespace with "Microsoft." as the value. + var newNamespace = new ReservedNamespace("Micro", isSharedNamespace: false, isPrefix: true); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await Assert.ThrowsAsync(async () => await service.AddReservedNamespaceAsync(newNamespace)); + } + + [Fact] + public async Task LiberalNamespaceForExactMatchIsAllowed() + { + var testNamespaces = GetTestNamespaces(); + // test case has a namespace with "Microsoft." as the value. + var newNamespace = new ReservedNamespace("Micro", isSharedNamespace: false, isPrefix: false /*exact match*/); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await service.AddReservedNamespaceAsync(newNamespace); + + service.MockReservedNamespaceRepository.Verify( + x => x.InsertOnCommit( + It.Is( + rn => rn.Value == newNamespace.Value + && rn.IsPrefix == newNamespace.IsPrefix + && rn.IsSharedNamespace == newNamespace.IsSharedNamespace))); + + service.MockReservedNamespaceRepository.Verify(x => x.CommitChangesAsync()); + } + } + + public class TheDeleteReservedNamespaceAsyncMethod + { + [Fact] + public async Task VanillaReservedNamespaceIsDeletedCorrectly() + { + var testNamespaces = GetTestNamespaces(); + var existingNamespace = testNamespaces.First(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await service.DeleteReservedNamespaceAsync(existingNamespace.Value); + + service.MockReservedNamespaceRepository.Verify( + x => x.DeleteOnCommit( + It.Is( + rn => rn.Value == existingNamespace.Value + && rn.IsPrefix == existingNamespace.IsPrefix + && rn.IsSharedNamespace == existingNamespace.IsSharedNamespace))); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + service + .MockPackageService + .Verify(p => p.UpdatePackageVerifiedStatusAsync(It.IsAny>(), It.IsAny()), Times.Never); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task InvalidNamespaceThrowsException(string value) + { + var service = new TestableReservedNamespaceService(); + + await Assert.ThrowsAsync(async () => await service.DeleteReservedNamespaceAsync(value)); + } + + [Fact] + public async Task NonexistentNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await Assert.ThrowsAsync(async () => await service.DeleteReservedNamespaceAsync("Nonexistent.Namespace.")); + } + + [Fact] + public async Task DeletingNamespaceClearsVerifiedFlagOnPackage() + { + var namespaces = GetTestNamespaces(); + var registrations = GetRegistrations(); + var msPrefix = namespaces.First(); + msPrefix.PackageRegistrations = registrations.Where(x => x.Id.StartsWith(msPrefix.Value)).ToList(); + msPrefix.PackageRegistrations.ToList().ForEach(x => x.ReservedNamespaces.Add(msPrefix)); + + var service = new TestableReservedNamespaceService(reservedNamespaces: namespaces, packageRegistrations: registrations); + await service.DeleteReservedNamespaceAsync(msPrefix.Value); + + var registrationsShouldUpdate = msPrefix.PackageRegistrations; + service + .MockReservedNamespaceRepository + .Verify( + x => x.DeleteOnCommit( + It.Is( + rn => rn.Value == msPrefix.Value + && rn.IsPrefix == msPrefix.IsPrefix + && rn.IsSharedNamespace == msPrefix.IsSharedNamespace))); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + service + .MockPackageService + .Verify( + p => p.UpdatePackageVerifiedStatusAsync( + It.Is>( + list => list.Count() == registrationsShouldUpdate.Count() + && list.Any(pr => registrationsShouldUpdate.Any(ru => ru.Id == pr.Id))), + false), + Times.Once); + + msPrefix.PackageRegistrations.ToList().ForEach(rn => Assert.False(rn.IsVerified)); + } + } + + public class TheAddOwnerToReservedNamespaceAsyncMethod + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task InvalidNamespaceThrowsException(string value) + { + var service = new TestableReservedNamespaceService(); + + await Assert.ThrowsAsync(async () => await service.AddOwnerToReservedNamespaceAsync(value, "test1")); + } + + [Fact] + public async Task NonExistentNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var testUsers = GetTestUsers(); + var existingUser = testUsers.First(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await Assert.ThrowsAsync(async () => await service.AddOwnerToReservedNamespaceAsync("NonExistent.Namespace.", existingUser.Username)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task AddingInvalidOwnerToNamespaceThrowsException(string username) + { + var testNamespaces = GetTestNamespaces(); + var existingNamespace = testNamespaces.First(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await Assert.ThrowsAsync(async () => await service.AddOwnerToReservedNamespaceAsync(existingNamespace.Value, username)); + } + + [Fact] + public async Task AddingNonExistentUserToNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var existingNamespace = testNamespaces.First(); + var testUsers = GetTestUsers(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await Assert.ThrowsAsync(async () => await service.AddOwnerToReservedNamespaceAsync(existingNamespace.Value, "NonExistentUser")); + } + + [Fact] + public async Task AddAnOwnerToNamespaceSuccessfully() + { + var testNamespaces = GetTestNamespaces(); + var prefix = "microsoft."; + var existingNamespace = testNamespaces.FirstOrDefault(rn => rn.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase)); + var testUsers = GetTestUsers(); + var owner = testUsers.First(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await service.AddOwnerToReservedNamespaceAsync(prefix, owner.Username); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + + service + .MockPackageService + .Verify(p => p.UpdatePackageVerifiedStatusAsync( + It.IsAny>(), It.IsAny()), + Times.Never); + + Assert.True(existingNamespace.Owners.Contains(owner)); + } + + [Fact] + public async Task AddingOwnerToNamespaceMarksRegistrationsVerified() + { + var testNamespaces = GetTestNamespaces(); + var prefix = "microsoft."; + var existingNamespace = testNamespaces.FirstOrDefault(rn => rn.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase)); + var testUsers = GetTestUsers(); + var owner1 = testUsers.First(u => u.Username == "test1"); + var owner2 = testUsers.First(u => u.Username == "test2"); + var registrations = GetRegistrations(); + var pr1 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.Package1")); + var pr2 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.Package2")); + var pr3 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.AspNet.Package2")); + pr1.Owners.Add(owner1); + pr2.Owners.Add(owner1); + pr3.Owners.Add(owner2); + + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers, packageRegistrations: registrations); + + Assert.True(existingNamespace.PackageRegistrations.Count() == 0); + + await service.AddOwnerToReservedNamespaceAsync(prefix, owner1.Username); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + + service + .MockPackageService + .Verify(p => p.UpdatePackageVerifiedStatusAsync( + It.IsAny>(), It.IsAny()), + Times.Once); + + Assert.True(existingNamespace.Owners.Contains(owner1)); + // Only Microsoft.Package1 should match the namespace + Assert.True(existingNamespace.PackageRegistrations.Count() == 2); + existingNamespace + .PackageRegistrations + .ToList() + .ForEach(pr => + { + Assert.True(pr.IsVerified); + Assert.True(pr.Id == pr1.Id || pr.Id == pr2.Id); + }); + } + } + + public class TheDeleteOwnerFromReservedNamespaceAsyncMethod + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task NullNamespaceThrowsException(string value) + { + var service = new TestableReservedNamespaceService(); + + await Assert.ThrowsAsync(async () => await service.DeleteOwnerFromReservedNamespaceAsync(value, "test1")); + } + + [Fact] + public async Task NonExistentNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var testUsers = GetTestUsers(); + var existingUser = testUsers.First(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await Assert.ThrowsAsync(async () => await service.DeleteOwnerFromReservedNamespaceAsync("Non.Existent.Namespace.", existingUser.Username)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task DeletingInvalidOwnerFromNamespaceThrowsException(string username) + { + var testNamespaces = GetTestNamespaces(); + var existingNamespace = testNamespaces.First(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces); + + await Assert.ThrowsAsync(async () => await service.DeleteOwnerFromReservedNamespaceAsync(existingNamespace.Value, username)); + } + + [Fact] + public async Task DeletingNonExistentUserFromNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var existingNamespace = testNamespaces.First(); + var testUsers = GetTestUsers(); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await Assert.ThrowsAsync(async () => await service.DeleteOwnerFromReservedNamespaceAsync(existingNamespace.Value, "NonExistentUser")); + } + + [Fact] + public async Task DeletingNonOwnerFromNamespaceThrowsException() + { + var testNamespaces = GetTestNamespaces(); + var prefix = "microsoft."; + var existingNamespace = testNamespaces.FirstOrDefault(rn => rn.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase)); + var testUsers = GetTestUsers(); + var user1 = testUsers.First(u => u.Username == "test1"); + var user2 = testUsers.First(u => u.Username == "test2"); + existingNamespace.Owners.Add(user1); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await Assert.ThrowsAsync(async () => await service.DeleteOwnerFromReservedNamespaceAsync(prefix, user2.Username)); + } + + [Fact] + public async Task DeleteOwnerFromNamespaceSuccessfully() + { + var testNamespaces = GetTestNamespaces(); + var prefix = "microsoft."; + var existingNamespace = testNamespaces.FirstOrDefault(rn => rn.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase)); + var testUsers = GetTestUsers(); + var owner = testUsers.First(); + existingNamespace.Owners.Add(owner); + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers); + + await service.DeleteOwnerFromReservedNamespaceAsync(prefix, owner.Username); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + + service + .MockPackageService + .Verify(p => p.UpdatePackageVerifiedStatusAsync( + It.IsAny>(), It.IsAny()), + Times.Never); + + Assert.False(existingNamespace.Owners.Contains(owner)); + } + + [Fact] + public async Task DeletingOwnerFromNamespaceMarksRegistrationsUnverified() + { + var testNamespaces = GetTestNamespaces(); + var prefix = "microsoft."; + var existingNamespace = testNamespaces.FirstOrDefault(rn => rn.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase)); + var testUsers = GetTestUsers(); + var owner1 = testUsers.First(u => u.Username == "test1"); + var owner2 = testUsers.First(u => u.Username == "test2"); + var registrations = GetRegistrations(); + var pr1 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.Package1")); + var pr2 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.Package2")); + var pr3 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.AspNet.Package2")); + pr1.Owners.Add(owner1); + pr2.Owners.Add(owner1); + pr3.Owners.Add(owner2); + pr1.IsVerified = true; + pr2.IsVerified = true; + pr3.IsVerified = true; + existingNamespace.Owners.Add(owner1); + existingNamespace.PackageRegistrations.Add(pr1); + existingNamespace.PackageRegistrations.Add(pr2); + + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers, packageRegistrations: registrations); + + Assert.True(existingNamespace.PackageRegistrations.Count == 2); + + await service.DeleteOwnerFromReservedNamespaceAsync(prefix, owner1.Username); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + + service + .MockPackageService + .Verify(p => p.UpdatePackageVerifiedStatusAsync( + It.IsAny>(), It.IsAny()), + Times.Once); + + Assert.False(existingNamespace.Owners.Contains(owner1)); + Assert.True(existingNamespace.PackageRegistrations.Count == 0); + Assert.False(pr1.IsVerified); + Assert.False(pr2.IsVerified); + Assert.True(pr3.IsVerified); + } + + [Fact] + public async Task DeletingOwnerFromMultipleOwnedNamespaceDoesNotMarkPackagesUnVerfied() + { + var testNamespaces = GetTestNamespaces(); + var prefix = "microsoft."; + var existingNamespace = testNamespaces.FirstOrDefault(rn => rn.Value.Equals(prefix, StringComparison.OrdinalIgnoreCase)); + var testUsers = GetTestUsers(); + var owner1 = testUsers.First(u => u.Username == "test1"); + var owner2 = testUsers.First(u => u.Username == "test2"); + var registrations = GetRegistrations(); + var pr1 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.Package1")); + var pr2 = registrations.ToList().FirstOrDefault(pr => (pr.Id == "Microsoft.Package2")); + pr1.Owners.Add(owner1); + pr2.Owners.Add(owner1); + pr2.Owners.Add(owner2); + pr1.IsVerified = true; + pr2.IsVerified = true; + existingNamespace.Owners.Add(owner1); + existingNamespace.Owners.Add(owner2); + existingNamespace.PackageRegistrations.Add(pr1); + existingNamespace.PackageRegistrations.Add(pr2); + + var service = new TestableReservedNamespaceService(reservedNamespaces: testNamespaces, users: testUsers, packageRegistrations: registrations); + + Assert.True(existingNamespace.PackageRegistrations.Count == 2); + + await service.DeleteOwnerFromReservedNamespaceAsync(prefix, owner1.Username); + + service + .MockReservedNamespaceRepository + .Verify(x => x.CommitChangesAsync()); + + service + .MockPackageService + .Verify(p => p.UpdatePackageVerifiedStatusAsync( + It.IsAny>(), It.IsAny()), + Times.Once); + + Assert.False(existingNamespace.Owners.Contains(owner1)); + Assert.True(existingNamespace.PackageRegistrations.Count == 1); + Assert.False(pr1.IsVerified); + Assert.True(pr2.IsVerified); + } + } + + public class ValidateNamespaceMethod + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("@startsWithSpecialChars")] + [InlineData("ends.With.Special.Char$")] + [InlineData("Cont@ins$pecia|C#aracters")] + [InlineData("Endswithperods..")] + [InlineData("Multiple.Sequential..Periods.")] + public void InvalidNamespacesThrowsException(string value) + { + Assert.Throws(() => ReservedNamespaceService.ValidateNamespace(value)); + } + + [Theory] + [InlineData("Namespace")] + [InlineData("Nam-e_s.pace")] + [InlineData("Name.Space.")] + [InlineData("123_Name.space.")] + [InlineData("123-Namespace.")] + public void ValidNamespacesDontThrowException(string value) + { + var ex = Record.Exception(() => ReservedNamespaceService.ValidateNamespace(value)); + Assert.Null(ex); + } + } + + private static IList GetTestNamespaces() + { + var result = new List(); + result.Add(new ReservedNamespace("Microsoft.", isSharedNamespace: false, isPrefix: true)); + result.Add(new ReservedNamespace("microsoft.aspnet.", isSharedNamespace: false, isPrefix: true)); + result.Add(new ReservedNamespace("baseTest.", isSharedNamespace: false, isPrefix: true)); + result.Add(new ReservedNamespace("jquery", isSharedNamespace: false, isPrefix: false)); + result.Add(new ReservedNamespace("jquery.Extentions.", isSharedNamespace: true, isPrefix: true)); + result.Add(new ReservedNamespace("Random.", isSharedNamespace: false, isPrefix: true)); + + return result; + } + + private static IList GetRegistrations() + { + var result = new List(); + result.Add(new PackageRegistration { Id = "Microsoft.Package1", IsVerified = false }); + result.Add(new PackageRegistration { Id = "Microsoft.Package2", IsVerified = false }); + result.Add(new PackageRegistration { Id = "Microsoft.AspNet.Package2", IsVerified = false }); + result.Add(new PackageRegistration { Id = "Random.Package1", IsVerified = false }); + result.Add(new PackageRegistration { Id = "jQuery", IsVerified = false }); + result.Add(new PackageRegistration { Id = "jQuery.Extentions.OwnerView", IsVerified = false }); + result.Add(new PackageRegistration { Id = "jQuery.Extentions.ThirdPartyView", IsVerified = false }); + result.Add(new PackageRegistration { Id = "DeltaX.Test1", IsVerified = false }); + + return result; + } + + private static IList GetTestUsers() + { + var result = new List(); + result.Add(new User("test1")); + result.Add(new User("test2")); + result.Add(new User("test3")); + result.Add(new User("test4")); + result.Add(new User("test5")); + + return result; + } + } +} diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 06cc0f2228..5a1d1ff2dd 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -2,14 +2,12 @@ // 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 Moq; -using NuGetGallery.Configuration; using NuGetGallery.Framework; using NuGetGallery.Auditing; using Xunit; +using NuGetGallery.TestUtils; namespace NuGetGallery { @@ -361,46 +359,6 @@ public async Task ThrowsArgumentExceptionForNullUser() await ContractAssert.ThrowsArgNullAsync(async () => await service.ChangeEmailSubscriptionAsync(null, emailAllowed: true, notifyPackagePushed: true), "user"); } } - - public class TestableUserService : UserService - { - public Mock MockConfig { get; protected set; } - public Mock> MockUserRepository { get; protected set; } - public Mock> MockCredentialRepository { get; protected set; } - - public TestableUserService() - { - Config = (MockConfig = new Mock()).Object; - UserRepository = (MockUserRepository = new Mock>()).Object; - CredentialRepository = (MockCredentialRepository = new Mock>()).Object; - Auditing = new TestAuditingService(); - - // Set ConfirmEmailAddress to a default of true - MockConfig.Setup(c => c.ConfirmEmailAddresses).Returns(true); - } - } - - public class TestableUserServiceWithDBFaking : UserService - { - public Mock MockConfig { get; protected set; } - - public FakeEntitiesContext FakeEntitiesContext { get; set; } - - public IEnumerable Users - { - set - { - foreach (User u in value) FakeEntitiesContext.Set().Add(u); - } - } - - public TestableUserServiceWithDBFaking() - { - Config = (MockConfig = new Mock()).Object; - UserRepository = new EntityRepository(FakeEntitiesContext = new FakeEntitiesContext()); - Auditing = new TestAuditingService(); - } - } } } diff --git a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs index 9afd485f80..f3f5b9657c 100644 --- a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs +++ b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs @@ -110,6 +110,18 @@ public IDbSet UserSecurityPolicies } } + public IDbSet ReservedNamespaces + { + get + { + return Set(); + } + set + { + throw new NotSupportedException(); + } + } + public Task SaveChangesAsync() { _areChangesSaved = true; diff --git a/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs b/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs new file mode 100644 index 0000000000..963d0d2e37 --- /dev/null +++ b/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs @@ -0,0 +1,172 @@ +using Moq; +using NuGetGallery.Configuration; +using NuGetGallery.Framework; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; + +namespace NuGetGallery.TestUtils +{ + public static class PackageServiceUtility + { + private const string _packageHashForTests = "NzMzMS1QNENLNEczSDQ1SA=="; + + public static Package CreateTestPackage(string id = null) + { + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = string.IsNullOrEmpty(id) ? "test" : id; + + var framework = new PackageFramework(); + var author = new PackageAuthor { Name = "maarten" }; + var dependency = new PackageDependency { Id = "other", VersionSpec = "1.0.0" }; + + var package = new Package + { + Key = 123, + PackageRegistration = packageRegistration, + Version = "1.0.0", + Hash = _packageHashForTests, + SupportedFrameworks = new List + { + framework + }, + FlattenedAuthors = "maarten", + Authors = new List + { + author + }, + Dependencies = new List + { + dependency + }, + User = new User("test") + }; + + packageRegistration.Packages.Add(package); + + return package; + } + } + + public class TestableUserService : UserService + { + public Mock MockConfig { get; protected set; } + public Mock> MockUserRepository { get; protected set; } + public Mock> MockCredentialRepository { get; protected set; } + + public TestableUserService() + { + Config = (MockConfig = new Mock()).Object; + UserRepository = (MockUserRepository = new Mock>()).Object; + CredentialRepository = (MockCredentialRepository = new Mock>()).Object; + Auditing = new TestAuditingService(); + + // Set ConfirmEmailAddress to a default of true + MockConfig.Setup(c => c.ConfirmEmailAddresses).Returns(true); + } + } + + public class TestableUserServiceWithDBFaking : UserService + { + public Mock MockConfig { get; protected set; } + + public FakeEntitiesContext FakeEntitiesContext { get; set; } + + public IEnumerable Users + { + set + { + foreach (User u in value) FakeEntitiesContext.Set().Add(u); + } + } + + public TestableUserServiceWithDBFaking(FakeEntitiesContext context = null) + { + FakeEntitiesContext = context ?? new FakeEntitiesContext(); + Config = (MockConfig = new Mock()).Object; + UserRepository = new EntityRepository(FakeEntitiesContext); + Auditing = new TestAuditingService(); + } + } + + public class TestableReservedNamespaceService : ReservedNamespaceService + { + public Mock MockPackageService; + public Mock> MockReservedNamespaceRepository; + + public IEnumerable ReservedNamespaces; + public IEnumerable PackageRegistrations; + public IEnumerable Users; + + public TestableReservedNamespaceService( + IList reservedNamespaces = null, + IList packageRegistrations = null, + IList users = null) + { + ReservedNamespaces = reservedNamespaces ?? new List(); + PackageRegistrations = packageRegistrations ?? new List(); + Users = users ?? new List(); + + EntitiesContext = SetupEntitiesContext().Object; + + MockReservedNamespaceRepository = SetupReservedNamespaceRepository(); + ReservedNamespaceRepository = MockReservedNamespaceRepository.Object; + + MockPackageService = SetupPackageService(); + PackageService = MockPackageService.Object; + + UserService = new TestableUserServiceWithDBFaking(); + ((TestableUserServiceWithDBFaking)UserService).Users = Users; + + AuditingService = new TestAuditingService(); + } + + private Mock> SetupReservedNamespaceRepository() + { + var obj = new Mock>(); + + obj.Setup(x => x.GetAll()) + .Returns(ReservedNamespaces.AsQueryable()); + + return obj; + } + + private Mock SetupEntitiesContext() + { + var mockContext = new Mock(); + var dbContext = new Mock(); + mockContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + + return mockContext; + } + + private Mock SetupPackageService() + { + var packageRegistrationRepository = new Mock>(); + packageRegistrationRepository + .Setup(x => x.GetAll()) + .Returns(PackageRegistrations.AsQueryable()) + .Verifiable(); + + var packageRepository = new Mock>(); + var packageOwnerRequestRepo = new Mock>(); + var indexingService = new Mock(); + var packageNamingConflictValidator = new PackageNamingConflictValidator( + packageRegistrationRepository.Object, + packageRepository.Object); + var auditingService = new TestAuditingService(); + + var packageService = new Mock( + packageRegistrationRepository.Object, + packageRepository.Object, + packageOwnerRequestRepo.Object, + indexingService.Object, + packageNamingConflictValidator, + auditingService); + + packageService.CallBase = true; + + return packageService; + } + } +}