From 278b51c2a616ac03fe2f025bb03c4e175c8947d3 Mon Sep 17 00:00:00 2001 From: enmande <3836813+enmande@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:21:51 -0500 Subject: [PATCH] test(emergency-access) [PM-29584]: Add mailer-specific tests. --- .../EmergencyAccessMailTests.cs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 test/Core.Test/Auth/UserFeatures/EmergencyAccess/EmergencyAccessMailTests.cs diff --git a/test/Core.Test/Auth/UserFeatures/EmergencyAccess/EmergencyAccessMailTests.cs b/test/Core.Test/Auth/UserFeatures/EmergencyAccess/EmergencyAccessMailTests.cs new file mode 100644 index 0000000000..aa3c778fa5 --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/EmergencyAccess/EmergencyAccessMailTests.cs @@ -0,0 +1,144 @@ +using Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail; +using Bit.Core.Models.Mail; +using Bit.Core.Platform.Mail.Delivery; +using Bit.Core.Platform.Mail.Mailer; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + +namespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess; + +[SutProviderCustomize] +public class EmergencyAccessMailTests +{ + /// + /// Documents how to construct and send the emergency access removal email. + /// 1. Inject IMailer into their command/service + /// 2. Get WebVaultUrl from GlobalSettings.BaseServiceUri.VaultWithHash + /// 3. Construct EmergencyAccessRemoveGranteesMail as shown below + /// 4. Call mailer.SendEmail(mail) + /// + [Theory, BitAutoData] + public async Task SendEmergencyAccessRemoveGranteesEmail_SingleGrantee_Success( + string grantorEmail, + string granteeName, + string webVaultUrl) + { + // Arrange + var logger = Substitute.For>(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + var deliveryService = Substitute.For(); + var mailer = new Mailer( + new HandlebarMailRenderer(logger, globalSettings), + deliveryService); + + var mail = new EmergencyAccessRemoveGranteesMail + { + ToEmails = [grantorEmail], + View = new EmergencyAccessRemoveGranteesMailView + { + RemovedGranteeNames = [granteeName], + WebVaultUrl = webVaultUrl + } + }; + + MailMessage sentMessage = null; + await deliveryService.SendEmailAsync(Arg.Do(message => + sentMessage = message + )); + + // Act + await mailer.SendEmail(mail); + + // Assert + Assert.NotNull(sentMessage); + Assert.Contains(grantorEmail, sentMessage.ToEmails); + Assert.Equal("Emergency contacts removed", sentMessage.Subject); + + // Verify the content contains the grantee name + Assert.Contains(granteeName, sentMessage.TextContent); + Assert.Contains(granteeName, sentMessage.HtmlContent); + + // Verify the vault link is present + Assert.Contains(webVaultUrl, sentMessage.HtmlContent); + Assert.Contains("web app", sentMessage.HtmlContent); + } + + /// + /// Documents handling multiple removed grantees in a single email. + /// + [Theory, BitAutoData] + public async Task SendEmergencyAccessRemoveGranteesEmail_MultipleGrantees_RendersAllNames( + string grantorEmail, + string webVaultUrl) + { + // Arrange + var logger = Substitute.For>(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + var deliveryService = Substitute.For(); + var mailer = new Mailer( + new HandlebarMailRenderer(logger, globalSettings), + deliveryService); + + var granteeNames = new[] { "Alice", "Bob", "Carol" }; + + var mail = new EmergencyAccessRemoveGranteesMail + { + ToEmails = [grantorEmail], + View = new EmergencyAccessRemoveGranteesMailView + { + RemovedGranteeNames = granteeNames, + WebVaultUrl = webVaultUrl + } + }; + + MailMessage sentMessage = null; + await deliveryService.SendEmailAsync(Arg.Do(message => + sentMessage = message + )); + + // Act + await mailer.SendEmail(mail); + + // Assert - All grantee names should appear in the email + Assert.NotNull(sentMessage); + foreach (var granteeName in granteeNames) + { + Assert.Contains(granteeName, sentMessage.TextContent); + Assert.Contains(granteeName, sentMessage.HtmlContent); + } + } + + /// + /// Validates the minimal required fields for the email view model. + /// Both RemovedGranteeNames and WebVaultUrl are marked as 'required' in the view model. + /// + [Theory, BitAutoData] + public void EmergencyAccessRemoveGranteesMailView_RequiredFields_MustBeProvided( + string grantorEmail, + string webVaultUrl) + { + // Arrange - Shows the minimum required to construct the email + var mail = new EmergencyAccessRemoveGranteesMail + { + ToEmails = [grantorEmail], // Required: who to send to + View = new EmergencyAccessRemoveGranteesMailView + { + // Required: at least one removed grantee name + RemovedGranteeNames = ["Example Grantee"], + // Required: link to vault for managing emergency contacts + // In production: use GlobalSettings.BaseServiceUri.VaultWithHash + WebVaultUrl = webVaultUrl + } + }; + + // Assert - If this compiles and constructs, required fields are satisfied + Assert.NotNull(mail); + Assert.NotNull(mail.View); + Assert.NotEmpty(mail.View.RemovedGranteeNames); + Assert.NotNull(mail.View.WebVaultUrl); + Assert.Equal("Emergency contacts removed", mail.Subject); + } +}