Skip to content

Commit

Permalink
Send package published notification to package owner(s) when new vers…
Browse files Browse the repository at this point in the history
…ion is published

Send package published notification to package owner(s) when new version
is published
  • Loading branch information
maartenba committed Mar 30, 2016
1 parent 12e96a0 commit 6273d4e
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 1 deletion.
11 changes: 10 additions & 1 deletion src/NuGetGallery/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public partial class ApiController
public IIndexingService IndexingService { get; set; }
public IAutomaticallyCuratePackageCommand AutoCuratePackage { get; set; }
public IStatusService StatusService { get; set; }
public IMessageService MessageService { get; set; }

protected ApiController()
{
Expand All @@ -52,6 +53,7 @@ public ApiController(
ISearchService searchService,
IAutomaticallyCuratePackageCommand autoCuratePackage,
IStatusService statusService,
IMessageService messageService,
IAppConfiguration config)
{
EntitiesContext = entitiesContext;
Expand All @@ -65,6 +67,7 @@ public ApiController(
SearchService = searchService;
AutoCuratePackage = autoCuratePackage;
StatusService = statusService;
MessageService = messageService;
_config = config;
}

Expand All @@ -80,8 +83,9 @@ public ApiController(
IAutomaticallyCuratePackageCommand autoCuratePackage,
IStatusService statusService,
IStatisticsService statisticsService,
IMessageService messageService,
IAppConfiguration config)
: this(entitiesContext, packageService, packageFileService, userService, nugetExeDownloaderService, contentService, indexingService, searchService, autoCuratePackage, statusService, config)
: this(entitiesContext, packageService, packageFileService, userService, nugetExeDownloaderService, contentService, indexingService, searchService, autoCuratePackage, statusService, messageService, config)
{
StatisticsService = statisticsService;
}
Expand Down Expand Up @@ -286,6 +290,11 @@ private async Task<ActionResult> CreatePackageInternal()
IndexingService.UpdatePackage(package);
}

MessageService.SendPackageAddedNotice(package,
Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme),
Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme),
Url.Action("Account", "Users", routeValues: null, protocol: Request.Url.Scheme));

return new HttpStatusCodeResult(HttpStatusCode.Created);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/NuGetGallery/Controllers/PackagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,11 @@ public virtual async Task<ActionResult> VerifyPackage(VerifyPackageRequest formD

// tell Lucene to update index for the new package
_indexingService.UpdateIndex();

_messageService.SendPackageAddedNotice(package,
Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme),
Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme),
Url.Action("Account", "Users", routeValues: null, protocol: Request.Url.Scheme));
}

// delete the uploaded binary in the Uploads container
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/Services/IMessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ public interface IMessageService
void SendCredentialRemovedNotice(User user, Credential removed);
void SendCredentialAddedNotice(User user, Credential added);
void SendContactSupportEmail(ContactSupportRequest request);
void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl);
}
}
38 changes: 38 additions & 0 deletions src/NuGetGallery/Services/MessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,44 @@ private void SendMessage(MailMessage mailMessage, bool copySender = false)
}
}

public void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl)
{
string subject = "[{0}] Package published - {1} {2}";
string body = @"The package [{1} {2}]({3}) was just published on {0}. If this was not intended, please [contact support]({4}).
-----------------------------------------------
<em style=""font-size: 0.8em;"">
To stop receiving emails as an owner of this package, sign in to the {0} and
[change your email notification settings]({5}).
</em>";

body = String.Format(
CultureInfo.CurrentCulture,
body,
Config.GalleryOwner.DisplayName,
package.PackageRegistration.Id,
package.Version,
packageUrl,
packageSupportUrl,
emailSettingsUrl);

subject = String.Format(CultureInfo.CurrentCulture, subject, Config.GalleryOwner.DisplayName, package.PackageRegistration.Id, package.Version);

using (var mailMessage = new MailMessage())
{
mailMessage.Subject = subject;
mailMessage.Body = body;
mailMessage.From = Config.GalleryOwner;

AddOwnersToMailMessage(package.PackageRegistration, mailMessage);

if (mailMessage.To.Any())
{
SendMessage(mailMessage);
}
}
}

private static void AddOwnersToMailMessage(PackageRegistration packageRegistration, MailMessage mailMessage)
{
foreach (var owner in packageRegistration.Owners.Where(o => o.EmailAllowed))
Expand Down
40 changes: 40 additions & 0 deletions tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class TestableApiController : ApiController
public Mock<IStatisticsService> MockStatisticsService { get; private set; }
public Mock<IIndexingService> MockIndexingService { get; private set; }
public Mock<IAutomaticallyCuratePackageCommand> MockAutoCuratePackage { get; private set; }
public Mock<IMessageService> MockMessageService { get; private set; }

private Stream PackageFromInputStream { get; set; }

Expand All @@ -51,6 +52,8 @@ public TestableApiController(MockBehavior behavior = MockBehavior.Default)
.Returns(Task.CompletedTask);
PackageFileService = MockPackageFileService.Object;

MessageService = (MockMessageService = new Mock<IMessageService>()).Object;

TestUtility.SetupHttpContextMockForUrlGeneration(new Mock<HttpContextBase>(), this);
}

Expand Down Expand Up @@ -78,14 +81,21 @@ public async Task CreatePackageWillSavePackageFileToFileStorage()
// Arrange
var user = new User() { EmailAddress = "confirmed@email.com" };
var packageRegistration = new PackageRegistration();
packageRegistration.Id = "theId";
packageRegistration.Owners.Add(user);
var package = new Package();
package.PackageRegistration = packageRegistration;
package.Version = "1.0.42";
packageRegistration.Packages.Add(package);

var controller = new TestableApiController();
controller.SetCurrentUser(user);
controller.MockPackageFileService.Setup(p => p.SavePackageFileAsync(It.IsAny<Package>(), It.IsAny<Stream>()))
.Returns(Task.CompletedTask).Verifiable();
controller.MockPackageService.Setup(p => p.FindPackageRegistrationById(It.IsAny<string>()))
.Returns(packageRegistration);
controller.MockPackageService.Setup(p => p.CreatePackageAsync(It.IsAny<PackageArchiveReader>(), It.IsAny<PackageStreamMetadata>(), It.IsAny<User>(), false))
.Returns(Task.FromResult(package));

var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42");
controller.SetupPackageFromInputStream(nuGetPackage);
Expand All @@ -97,6 +107,36 @@ public async Task CreatePackageWillSavePackageFileToFileStorage()
controller.MockPackageFileService.Verify();
}

[Fact]
public async Task CreatePackageWillSendPackageAddedNotice()
{
// Arrange
var user = new User() { EmailAddress = "confirmed@email.com" };
var packageRegistration = new PackageRegistration();
packageRegistration.Id = "theId";
packageRegistration.Owners.Add(user);
var package = new Package();
package.PackageRegistration = packageRegistration;
package.Version = "1.0.42";
packageRegistration.Packages.Add(package);

var controller = new TestableApiController();
controller.SetCurrentUser(user);
controller.MockMessageService.Setup(p => p.SendPackageAddedNotice(package, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Verifiable();
controller.MockPackageService.Setup(p => p.CreatePackageAsync(It.IsAny<PackageArchiveReader>(), It.IsAny<PackageStreamMetadata>(), It.IsAny<User>(), false))
.Returns(Task.FromResult(package));

var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42");
controller.SetupPackageFromInputStream(nuGetPackage);

// Act
await controller.CreatePackagePut();

// Assert
controller.MockMessageService.Verify();
}

[Fact]
public async Task CreatePackageWillReturn400IfPackageIsInvalid()
{
Expand Down
97 changes: 97 additions & 0 deletions tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,103 @@ public void UsesTypeCaptionToDescribeCredentialIfNoProviderNounPresent()
}
}

public class TheSendPackageAddedNoticeMethod
{
[Fact]
public void WillSendEmailToAllOwners()
{
// Arrange
var packageRegistration = new PackageRegistration
{
Id = "smangit",
Owners = new[]
{
new User { EmailAddress = "yung@example.com", EmailAllowed = true },
new User { EmailAddress = "flynt@example.com", EmailAllowed = true }
}
};
var package = new Package
{
Version = "1.2.3",
PackageRegistration = packageRegistration
};
packageRegistration.Packages.Add(package);

// Act
var messageService = new TestableMessageService();
messageService.SendPackageAddedNotice(package, "http://dummy1", "http://dummy2", "http://dummy3");

// Assert
var message = messageService.MockMailSender.Sent.Last();

Assert.Equal("yung@example.com", message.To[0].Address);
Assert.Equal("flynt@example.com", message.To[1].Address);
Assert.Equal(TestGalleryOwner, message.From);
Assert.Contains("[Joe Shmoe] Package published - smangit 1.2.3", message.Subject);
Assert.Contains(
"The package [smangit 1.2.3](http://dummy1) was just published on Joe Shmoe. If this was not intended, please [contact support](http://dummy2).", message.Body);
}

[Fact]
public void WillNotSendEmailToOwnerThatOptsOut()
{
// Arrange
var packageRegistration = new PackageRegistration
{
Id = "smangit",
Owners = new[]
{
new User { EmailAddress = "yung@example.com", EmailAllowed = true },
new User { EmailAddress = "flynt@example.com", EmailAllowed = false }
}
};
var package = new Package
{
Version = "1.2.3",
PackageRegistration = packageRegistration
};
packageRegistration.Packages.Add(package);

// Act
var messageService = new TestableMessageService();
messageService.SendPackageAddedNotice(package, "http://dummy1", "http://dummy2", "http://dummy3");

// Assert
var message = messageService.MockMailSender.Sent.Last();

Assert.Equal("yung@example.com", message.To[0].Address);
Assert.Equal(1, message.To.Count);
}

[Fact]
public void WillNotAttemptToSendIfNoOwnersAllow()
{
// Arrange
var packageRegistration = new PackageRegistration
{
Id = "smangit",
Owners = new[]
{
new User { EmailAddress = "yung@example.com", EmailAllowed = false },
new User { EmailAddress = "flynt@example.com", EmailAllowed = false }
}
};
var package = new Package
{
Version = "1.2.3",
PackageRegistration = packageRegistration
};
packageRegistration.Packages.Add(package);

// Act
var messageService = new TestableMessageService();
messageService.SendPackageAddedNotice(package, "http://dummy1", "http://dummy2", "http://dummy3");

// Assert
Assert.Empty(messageService.MockMailSender.Sent);
}
}

public class TestableMessageService : MessageService
{
public Mock<AuthenticationService> MockAuthService { get; protected set; }
Expand Down

0 comments on commit 6273d4e

Please sign in to comment.