|
| 1 | +using Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail; |
| 2 | +using Bit.Core.Models.Mail; |
| 3 | +using Bit.Core.Platform.Mail.Delivery; |
| 4 | +using Bit.Core.Platform.Mail.Mailer; |
| 5 | +using Bit.Test.Common.AutoFixture.Attributes; |
| 6 | +using Microsoft.Extensions.Logging; |
| 7 | +using NSubstitute; |
| 8 | +using Xunit; |
| 9 | +using GlobalSettings = Bit.Core.Settings.GlobalSettings; |
| 10 | + |
| 11 | +namespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess; |
| 12 | + |
| 13 | +[SutProviderCustomize] |
| 14 | +public class EmergencyAccessMailTests |
| 15 | +{ |
| 16 | + /// <summary> |
| 17 | + /// Documents how to construct and send the emergency access removal email. |
| 18 | + /// 1. Inject IMailer into their command/service |
| 19 | + /// 2. Get WebVaultUrl from GlobalSettings.BaseServiceUri.VaultWithHash |
| 20 | + /// 3. Construct EmergencyAccessRemoveGranteesMail as shown below |
| 21 | + /// 4. Call mailer.SendEmail(mail) |
| 22 | + /// </summary> |
| 23 | + [Theory, BitAutoData] |
| 24 | + public async Task SendEmergencyAccessRemoveGranteesEmail_SingleGrantee_Success( |
| 25 | + string grantorEmail, |
| 26 | + string granteeName, |
| 27 | + string webVaultUrl) |
| 28 | + { |
| 29 | + // Arrange |
| 30 | + var logger = Substitute.For<ILogger<HandlebarMailRenderer>>(); |
| 31 | + var globalSettings = new GlobalSettings { SelfHosted = false }; |
| 32 | + var deliveryService = Substitute.For<IMailDeliveryService>(); |
| 33 | + var mailer = new Mailer( |
| 34 | + new HandlebarMailRenderer(logger, globalSettings), |
| 35 | + deliveryService); |
| 36 | + |
| 37 | + var mail = new EmergencyAccessRemoveGranteesMail |
| 38 | + { |
| 39 | + ToEmails = [grantorEmail], |
| 40 | + View = new EmergencyAccessRemoveGranteesMailView |
| 41 | + { |
| 42 | + RemovedGranteeNames = [granteeName], |
| 43 | + WebVaultUrl = webVaultUrl |
| 44 | + } |
| 45 | + }; |
| 46 | + |
| 47 | + MailMessage sentMessage = null; |
| 48 | + await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message => |
| 49 | + sentMessage = message |
| 50 | + )); |
| 51 | + |
| 52 | + // Act |
| 53 | + await mailer.SendEmail(mail); |
| 54 | + |
| 55 | + // Assert |
| 56 | + Assert.NotNull(sentMessage); |
| 57 | + Assert.Contains(grantorEmail, sentMessage.ToEmails); |
| 58 | + Assert.Equal("Emergency contacts removed", sentMessage.Subject); |
| 59 | + |
| 60 | + // Verify the content contains the grantee name |
| 61 | + Assert.Contains(granteeName, sentMessage.TextContent); |
| 62 | + Assert.Contains(granteeName, sentMessage.HtmlContent); |
| 63 | + |
| 64 | + // Verify the vault link is present |
| 65 | + Assert.Contains(webVaultUrl, sentMessage.HtmlContent); |
| 66 | + Assert.Contains("web app", sentMessage.HtmlContent); |
| 67 | + } |
| 68 | + |
| 69 | + /// <summary> |
| 70 | + /// Documents handling multiple removed grantees in a single email. |
| 71 | + /// </summary> |
| 72 | + [Theory, BitAutoData] |
| 73 | + public async Task SendEmergencyAccessRemoveGranteesEmail_MultipleGrantees_RendersAllNames( |
| 74 | + string grantorEmail, |
| 75 | + string webVaultUrl) |
| 76 | + { |
| 77 | + // Arrange |
| 78 | + var logger = Substitute.For<ILogger<HandlebarMailRenderer>>(); |
| 79 | + var globalSettings = new GlobalSettings { SelfHosted = false }; |
| 80 | + var deliveryService = Substitute.For<IMailDeliveryService>(); |
| 81 | + var mailer = new Mailer( |
| 82 | + new HandlebarMailRenderer(logger, globalSettings), |
| 83 | + deliveryService); |
| 84 | + |
| 85 | + var granteeNames = new[] { "Alice", "Bob", "Carol" }; |
| 86 | + |
| 87 | + var mail = new EmergencyAccessRemoveGranteesMail |
| 88 | + { |
| 89 | + ToEmails = [grantorEmail], |
| 90 | + View = new EmergencyAccessRemoveGranteesMailView |
| 91 | + { |
| 92 | + RemovedGranteeNames = granteeNames, |
| 93 | + WebVaultUrl = webVaultUrl |
| 94 | + } |
| 95 | + }; |
| 96 | + |
| 97 | + MailMessage sentMessage = null; |
| 98 | + await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message => |
| 99 | + sentMessage = message |
| 100 | + )); |
| 101 | + |
| 102 | + // Act |
| 103 | + await mailer.SendEmail(mail); |
| 104 | + |
| 105 | + // Assert - All grantee names should appear in the email |
| 106 | + Assert.NotNull(sentMessage); |
| 107 | + foreach (var granteeName in granteeNames) |
| 108 | + { |
| 109 | + Assert.Contains(granteeName, sentMessage.TextContent); |
| 110 | + Assert.Contains(granteeName, sentMessage.HtmlContent); |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + /// <summary> |
| 115 | + /// Validates the minimal required fields for the email view model. |
| 116 | + /// Both RemovedGranteeNames and WebVaultUrl are marked as 'required' in the view model. |
| 117 | + /// </summary> |
| 118 | + [Theory, BitAutoData] |
| 119 | + public void EmergencyAccessRemoveGranteesMailView_RequiredFields_MustBeProvided( |
| 120 | + string grantorEmail, |
| 121 | + string webVaultUrl) |
| 122 | + { |
| 123 | + // Arrange - Shows the minimum required to construct the email |
| 124 | + var mail = new EmergencyAccessRemoveGranteesMail |
| 125 | + { |
| 126 | + ToEmails = [grantorEmail], // Required: who to send to |
| 127 | + View = new EmergencyAccessRemoveGranteesMailView |
| 128 | + { |
| 129 | + // Required: at least one removed grantee name |
| 130 | + RemovedGranteeNames = ["Example Grantee"], |
| 131 | + // Required: link to vault for managing emergency contacts |
| 132 | + // In production: use GlobalSettings.BaseServiceUri.VaultWithHash |
| 133 | + WebVaultUrl = webVaultUrl |
| 134 | + } |
| 135 | + }; |
| 136 | + |
| 137 | + // Assert - If this compiles and constructs, required fields are satisfied |
| 138 | + Assert.NotNull(mail); |
| 139 | + Assert.NotNull(mail.View); |
| 140 | + Assert.NotEmpty(mail.View.RemovedGranteeNames); |
| 141 | + Assert.NotNull(mail.View.WebVaultUrl); |
| 142 | + Assert.Equal("Emergency contacts removed", mail.Subject); |
| 143 | + } |
| 144 | +} |
0 commit comments