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);
+ }
+}