mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
* Added docs for legacy mail service. * Updated namespaces. * Consolidated under Platform.Mail namespace * Updated obsolete comment. * Linting * Linting * Replaced documentation in original readme after accidental deletion.
272 lines
11 KiB
C#
272 lines
11 KiB
C#
using System.Reflection;
|
|
using Bit.Core.AdminConsole.Entities;
|
|
using Bit.Core.AdminConsole.Entities.Provider;
|
|
using Bit.Core.Auth.Entities;
|
|
using Bit.Core.Auth.Enums;
|
|
using Bit.Core.Auth.Models.Business;
|
|
using Bit.Core.Entities;
|
|
using Bit.Core.Models.Mail;
|
|
using Bit.Core.Platform.Mail.Delivery;
|
|
using Bit.Core.Platform.Mail.Enqueuing;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Services.Mail;
|
|
using Bit.Core.Settings;
|
|
using Microsoft.Extensions.Caching.Distributed;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace Bit.Core.Test.Services;
|
|
|
|
public class HandlebarsMailServiceTests
|
|
{
|
|
private readonly HandlebarsMailService _sut;
|
|
|
|
private readonly GlobalSettings _globalSettings;
|
|
private readonly IMailDeliveryService _mailDeliveryService;
|
|
private readonly IMailEnqueuingService _mailEnqueuingService;
|
|
private readonly IDistributedCache _distributedCache;
|
|
private readonly ILogger<HandlebarsMailService> _logger;
|
|
|
|
public HandlebarsMailServiceTests()
|
|
{
|
|
_globalSettings = new GlobalSettings();
|
|
_mailDeliveryService = Substitute.For<IMailDeliveryService>();
|
|
_mailEnqueuingService = Substitute.For<IMailEnqueuingService>();
|
|
_distributedCache = Substitute.For<IDistributedCache>();
|
|
_logger = Substitute.For<ILogger<HandlebarsMailService>>();
|
|
|
|
_sut = new HandlebarsMailService(
|
|
_globalSettings,
|
|
_mailDeliveryService,
|
|
_mailEnqueuingService,
|
|
_distributedCache,
|
|
_logger
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendFailedTwoFactorAttemptEmailAsync_FirstCall_SendsEmail()
|
|
{
|
|
// Arrange
|
|
var email = "test@example.com";
|
|
var failedType = TwoFactorProviderType.Email;
|
|
var utcNow = DateTime.UtcNow;
|
|
var ip = "192.168.1.1";
|
|
|
|
_distributedCache.GetAsync(Arg.Any<string>()).Returns((byte[])null);
|
|
|
|
// Act
|
|
await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip);
|
|
|
|
// Assert
|
|
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
|
await _distributedCache.Received(1).SetAsync(
|
|
Arg.Is<string>(key => key == $"FailedTwoFactorAttemptEmail_{email}"),
|
|
Arg.Any<byte[]>(),
|
|
Arg.Any<DistributedCacheEntryOptions>()
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendFailedTwoFactorAttemptEmailAsync_SecondCallWithinHour_DoesNotSendEmail()
|
|
{
|
|
// Arrange
|
|
var email = "test@example.com";
|
|
var failedType = TwoFactorProviderType.Email;
|
|
var utcNow = DateTime.UtcNow;
|
|
var ip = "192.168.1.1";
|
|
|
|
// Simulate cache hit (email was already sent)
|
|
_distributedCache.GetAsync(Arg.Any<string>()).Returns([1]);
|
|
|
|
// Act
|
|
await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip);
|
|
|
|
// Assert
|
|
await _mailDeliveryService.DidNotReceive().SendEmailAsync(Arg.Any<MailMessage>());
|
|
await _distributedCache.DidNotReceive().SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendFailedTwoFactorAttemptEmailAsync_DifferentEmails_SendsBothEmails()
|
|
{
|
|
// Arrange
|
|
var email1 = "test1@example.com";
|
|
var email2 = "test2@example.com";
|
|
var failedType = TwoFactorProviderType.Email;
|
|
var utcNow = DateTime.UtcNow;
|
|
var ip = "192.168.1.1";
|
|
|
|
_distributedCache.GetAsync(Arg.Any<string>()).Returns((byte[])null);
|
|
|
|
// Act
|
|
await _sut.SendFailedTwoFactorAttemptEmailAsync(email1, failedType, utcNow, ip);
|
|
await _sut.SendFailedTwoFactorAttemptEmailAsync(email2, failedType, utcNow, ip);
|
|
|
|
// Assert
|
|
await _mailDeliveryService.Received(2).SendEmailAsync(Arg.Any<MailMessage>());
|
|
await _distributedCache.Received(1).SetAsync(
|
|
Arg.Is<string>(key => key == $"FailedTwoFactorAttemptEmail_{email1}"),
|
|
Arg.Any<byte[]>(),
|
|
Arg.Any<DistributedCacheEntryOptions>()
|
|
);
|
|
await _distributedCache.Received(1).SetAsync(
|
|
Arg.Is<string>(key => key == $"FailedTwoFactorAttemptEmail_{email2}"),
|
|
Arg.Any<byte[]>(),
|
|
Arg.Any<DistributedCacheEntryOptions>()
|
|
);
|
|
}
|
|
|
|
[Fact(Skip = "For local development")]
|
|
public async Task SendAllEmails()
|
|
{
|
|
// This test is only opt in and is more for development purposes.
|
|
// This will send all emails to the test email address so that they can be viewed.
|
|
var namedParameters = new Dictionary<(string, Type), object>
|
|
{
|
|
// TODO: Switch to use env variable
|
|
{ ("email", typeof(string)), "test@bitwarden.com" },
|
|
{ ("user", typeof(User)), new User
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Email = "test@bitwarden.com",
|
|
}},
|
|
{ ("userId", typeof(Guid)), Guid.NewGuid() },
|
|
{ ("token", typeof(string)), "test_token" },
|
|
{ ("fromEmail", typeof(string)), "test@bitwarden.com" },
|
|
{ ("toEmail", typeof(string)), "test@bitwarden.com" },
|
|
{ ("newEmailAddress", typeof(string)), "test@bitwarden.com" },
|
|
{ ("hint", typeof(string)), "Test Hint" },
|
|
{ ("organizationName", typeof(string)), "Test Organization Name" },
|
|
{ ("orgUser", typeof(OrganizationUser)), new OrganizationUser
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Email = "test@bitwarden.com",
|
|
OrganizationId = Guid.NewGuid(),
|
|
|
|
}},
|
|
{ ("token", typeof(ExpiringToken)), new ExpiringToken("test_token", DateTime.UtcNow.AddDays(1))},
|
|
{ ("organization", typeof(Organization)), new Organization
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Test Organization Name",
|
|
Seats = 5
|
|
}},
|
|
{ ("initialSeatCount", typeof(int)), 5},
|
|
{ ("ownerEmails", typeof(IEnumerable<string>)), new [] { "test@bitwarden.com" }},
|
|
{ ("maxSeatCount", typeof(int)), 5 },
|
|
{ ("userIdentifier", typeof(string)), "test_user" },
|
|
{ ("adminEmails", typeof(IEnumerable<string>)), new [] { "test@bitwarden.com" }},
|
|
{ ("returnUrl", typeof(string)), "https://bitwarden.com/" },
|
|
{ ("amount", typeof(decimal)), 1.00M },
|
|
{ ("dueDate", typeof(DateTime)), DateTime.UtcNow.AddDays(1) },
|
|
{ ("items", typeof(List<string>)), new List<string> { "test@bitwarden.com" }},
|
|
{ ("mentionInvoices", typeof(bool)), true },
|
|
{ ("emails", typeof(IEnumerable<string>)), new [] { "test@bitwarden.com" }},
|
|
{ ("deviceType", typeof(string)), "Mobile" },
|
|
{ ("timestamp", typeof(DateTime)), DateTime.UtcNow.AddDays(1)},
|
|
{ ("ip", typeof(string)), "127.0.0.1" },
|
|
{ ("emergencyAccess", typeof(EmergencyAccess)), new EmergencyAccess
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Email = "test@bitwarden.com",
|
|
}},
|
|
{ ("granteeEmail", typeof(string)), "test@bitwarden.com" },
|
|
{ ("grantorName", typeof(string)), "Test User" },
|
|
{ ("initiatingName", typeof(string)), "Test" },
|
|
{ ("approvingName", typeof(string)), "Test Name" },
|
|
{ ("rejectingName", typeof(string)), "Test Name" },
|
|
{ ("provider", typeof(Provider)), new Provider
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
}},
|
|
{ ("name", typeof(string)), "Test Name" },
|
|
{ ("ea", typeof(EmergencyAccess)), new EmergencyAccess
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Email = "test@bitwarden.com",
|
|
}},
|
|
{ ("userName", typeof(string)), "testUser" },
|
|
{ ("orgName", typeof(string)), "Test Org Name" },
|
|
{ ("providerName", typeof(string)), "testProvider" },
|
|
{ ("providerUser", typeof(ProviderUser)), new ProviderUser
|
|
{
|
|
ProviderId = Guid.NewGuid(),
|
|
Id = Guid.NewGuid(),
|
|
}},
|
|
{ ("familyUserEmail", typeof(string)), "test@bitwarden.com" },
|
|
{ ("sponsorEmail", typeof(string)), "test@bitwarden.com" },
|
|
{ ("familyOrgName", typeof(string)), "Test Org Name" },
|
|
// Swap existingAccount to true or false to generate different versions of the SendFamiliesForEnterpriseOfferEmailAsync emails.
|
|
{ ("existingAccount", typeof(bool)), false },
|
|
{ ("sponsorshipEndDate", typeof(DateTime)), DateTime.UtcNow.AddDays(1)},
|
|
{ ("sponsorOrgName", typeof(string)), "Sponsor Test Org Name" },
|
|
{ ("expirationDate", typeof(DateTime)), DateTime.Now.AddDays(3) },
|
|
{ ("utcNow", typeof(DateTime)), DateTime.UtcNow },
|
|
};
|
|
|
|
var globalSettings = new GlobalSettings
|
|
{
|
|
Mail = new GlobalSettings.MailSettings
|
|
{
|
|
Smtp = new GlobalSettings.MailSettings.SmtpSettings
|
|
{
|
|
Host = "localhost",
|
|
TrustServer = true,
|
|
Port = 10250,
|
|
},
|
|
ReplyToEmail = "noreply@bitwarden.com",
|
|
},
|
|
SiteName = "Bitwarden",
|
|
};
|
|
|
|
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
|
|
var distributedCache = Substitute.For<IDistributedCache>();
|
|
var logger = Substitute.For<ILogger<HandlebarsMailService>>();
|
|
|
|
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache, logger);
|
|
|
|
var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync");
|
|
|
|
foreach (var sendMethod in sendMethods)
|
|
{
|
|
await InvokeMethod(sendMethod);
|
|
}
|
|
|
|
async Task InvokeMethod(MethodInfo method)
|
|
{
|
|
var parameters = method.GetParameters();
|
|
var args = new object[parameters.Length];
|
|
|
|
for (var i = 0; i < parameters.Length; i++)
|
|
{
|
|
if (!namedParameters.TryGetValue((parameters[i].Name, parameters[i].ParameterType), out var value))
|
|
{
|
|
throw new InvalidOperationException($"Couldn't find a parameter for name '{parameters[i].Name}' and type '{parameters[i].ParameterType.FullName}'");
|
|
}
|
|
|
|
args[i] = value;
|
|
}
|
|
|
|
await (Task)method.Invoke(handlebarsService, args);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendSendEmailOtpEmailAsync_SendsEmail()
|
|
{
|
|
// Arrange
|
|
var email = "test@example.com";
|
|
var token = "aToken";
|
|
var subject = string.Format("Your Bitwarden Send verification code is {0}", token);
|
|
|
|
// Act
|
|
await _sut.SendSendEmailOtpEmailAsync(email, token, subject);
|
|
|
|
// Assert
|
|
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
|
}
|
|
}
|