1
0
mirror of https://github.com/bitwarden/server synced 2026-01-14 22:43:19 +00:00

Merge branch 'main' into tools/pm-21918/send-authentication-commands

This commit is contained in:
✨ Audrey ✨
2025-08-25 14:55:45 -04:00
176 changed files with 14667 additions and 1455 deletions

View File

@@ -27,7 +27,9 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReceivedExtensions;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using Organization = Bit.Core.AdminConsole.Entities.Organization;
using OrganizationUser = Bit.Core.Entities.OrganizationUser;
@@ -40,139 +42,7 @@ public class OrganizationServiceTests
{
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task OrgImportCreateNewUsers(SutProvider<OrganizationService> sutProvider, Organization org, List<OrganizationUserUserDetails> existingUsers, List<ImportedOrganizationUser> newUsers)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
org.UseDirectory = true;
org.Seats = 10;
newUsers.Add(new ImportedOrganizationUser
{
Email = existingUsers.First().Email,
ExternalId = existingUsers.First().ExternalId
});
var expectedNewUsersCount = newUsers.Count - 1;
existingUsers.First().Type = OrganizationUserType.Owner;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
organizationUserRepository.GetManyDetailsByOrganizationAsync(org.Id)
.Returns(existingUsers);
organizationUserRepository.GetCountByOrganizationIdAsync(org.Id)
.Returns(existingUsers.Count);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(org.Id, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(org.Id).Returns(true);
await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => !users.Any()));
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default);
// Create new users
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.CreateManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == expectedNewUsersCount));
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)
.SendInvitesAsync(
Arg.Is<SendInvitesRequest>(
info => info.Users.Length == expectedNewUsersCount &&
info.Organization == org));
// Send events
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(events =>
events.Count() == expectedNewUsersCount));
}
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task OrgImportCreateNewUsersAndMarryExistingUser(SutProvider<OrganizationService> sutProvider, Organization org, List<OrganizationUserUserDetails> existingUsers,
List<ImportedOrganizationUser> newUsers)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
org.UseDirectory = true;
org.Seats = newUsers.Count + existingUsers.Count + 1;
var reInvitedUser = existingUsers.First();
reInvitedUser.ExternalId = null;
newUsers.Add(new ImportedOrganizationUser
{
Email = reInvitedUser.Email,
ExternalId = reInvitedUser.Email,
});
var expectedNewUsersCount = newUsers.Count - 1;
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id)
.Returns(existingUsers);
sutProvider.GetDependency<IOrganizationUserRepository>().GetCountByOrganizationIdAsync(org.Id)
.Returns(existingUsers.Count);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(reInvitedUser.Id)
.Returns(new OrganizationUser { Id = reInvitedUser.Id });
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(org.Id, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
var currentContext = sutProvider.GetDependency<ICurrentContext>();
currentContext.ManageUsers(org.Id).Returns(true);
await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default, default);
// Upserted existing user
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == 1));
// Created and invited new users
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.CreateManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == expectedNewUsersCount));
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(request =>
request.Users.Length == expectedNewUsersCount &&
request.Organization == org));
// Sent events
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(events =>
events.Count(e => e.Item2 == EventType.OrganizationUser_Invited) == expectedNewUsersCount));
}
[Theory]
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
@@ -1235,6 +1105,130 @@ public class OrganizationServiceTests
await sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organization.Id, OrganizationUserType.Custom);
}
[Theory, BitAutoData]
public async Task UpdateAsync_WhenValidOrganization_AndUpdateBillingIsTrue_UpdateStripeCustomerAndOrganization(Organization organization, SutProvider<OrganizationService> sutProvider)
{
// Arrange
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var eventService = sutProvider.GetDependency<IEventService>();
var requestOptionsReturned = new CustomerUpdateOptions
{
Email = organization.BillingEmail,
Description = organization.DisplayBusinessName(),
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
// This overwrites the existing custom fields for this organization
CustomFields =
[
new CustomerInvoiceSettingsCustomFieldOptions
{
Name = organization.SubscriberType(),
Value = organization.DisplayName()[..30]
}
]
},
};
organizationRepository
.GetByIdentifierAsync(organization.Identifier!)
.Returns(organization);
// Act
await sutProvider.Sut.UpdateAsync(organization, updateBilling: true);
// Assert
await organizationRepository
.Received(1)
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
await stripeAdapter
.Received(1)
.CustomerUpdateAsync(
Arg.Is<string>(id => id == organization.GatewayCustomerId),
Arg.Is<CustomerUpdateOptions>(options => options.Email == requestOptionsReturned.Email
&& options.Description == requestOptionsReturned.Description
&& options.InvoiceSettings.CustomFields.First().Name == requestOptionsReturned.InvoiceSettings.CustomFields.First().Name
&& options.InvoiceSettings.CustomFields.First().Value == requestOptionsReturned.InvoiceSettings.CustomFields.First().Value)); ;
await organizationRepository
.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org == organization));
await applicationCacheService
.Received(1)
.UpsertOrganizationAbilityAsync(Arg.Is<Organization>(org => org == organization));
await eventService
.Received(1)
.LogOrganizationEventAsync(Arg.Is<Organization>(org => org == organization),
Arg.Is<EventType>(e => e == EventType.Organization_Updated));
}
[Theory, BitAutoData]
public async Task UpdateAsync_WhenValidOrganization_AndUpdateBillingIsFalse_UpdateOrganization(Organization organization, SutProvider<OrganizationService> sutProvider)
{
// Arrange
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var eventService = sutProvider.GetDependency<IEventService>();
organizationRepository
.GetByIdentifierAsync(organization.Identifier!)
.Returns(organization);
// Act
await sutProvider.Sut.UpdateAsync(organization, updateBilling: false);
// Assert
await organizationRepository
.Received(1)
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
await organizationRepository
.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org == organization));
await applicationCacheService
.Received(1)
.UpsertOrganizationAbilityAsync(Arg.Is<Organization>(org => org == organization));
await eventService
.Received(1)
.LogOrganizationEventAsync(Arg.Is<Organization>(org => org == organization),
Arg.Is<EventType>(e => e == EventType.Organization_Updated));
}
[Theory, BitAutoData]
public async Task UpdateAsync_WhenOrganizationHasNoId_ThrowsApplicationException(Organization organization, SutProvider<OrganizationService> sutProvider)
{
// Arrange
organization.Id = Guid.Empty;
// Act/Assert
var exception = await Assert.ThrowsAnyAsync<ApplicationException>(() => sutProvider.Sut.UpdateAsync(organization));
Assert.Equal("Cannot create org this way. Call SignUpAsync.", exception.Message);
}
[Theory, BitAutoData]
public async Task UpdateAsync_WhenIdentifierAlreadyExistsForADifferentOrganization_ThrowsBadRequestException(Organization organization, SutProvider<OrganizationService> sutProvider)
{
// Arrange
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var differentOrganization = new Organization { Id = Guid.NewGuid() };
organizationRepository
.GetByIdentifierAsync(organization.Identifier!)
.Returns(differentOrganization);
// Act/Assert
var exception = await Assert.ThrowsAnyAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(organization));
Assert.Equal("Identifier already in use by another organization.", exception.Message);
await organizationRepository
.Received(1)
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
}
// Must set real guids in order for dictionary of guids to not throw aggregate exceptions
private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)
{

View File

@@ -21,7 +21,7 @@ namespace Bit.Core.Test.Billing.Organizations.Queries;
[SutProviderCustomize]
public class GetOrganizationWarningsQueryTests
{
private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"];
private static readonly string[] _requiredExpansions = ["customer.tax_ids", "latest_invoice", "test_clock"];
[Theory, BitAutoData]
public async Task Run_NoSubscription_NoWarnings(
@@ -130,7 +130,7 @@ public class GetOrganizationWarningsQueryTests
}
[Theory, BitAutoData]
public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethodOptionalTrial(
public async Task Run_OrganizationEnabled_NoInactiveSubscriptionWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
@@ -142,7 +142,7 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Trialing,
Status = StripeConstants.SubscriptionStatus.Unpaid,
Customer = new Customer
{
InvoiceSettings = new CustomerInvoiceSettings(),
@@ -151,14 +151,10 @@ public class GetOrganizationWarningsQueryTests
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
InactiveSubscription.Resolution: "add_payment_method_optional_trial"
});
Assert.Null(response.InactiveSubscription);
}
[Theory, BitAutoData]

View File

@@ -1695,9 +1695,6 @@ public class SubscriberServiceTests
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(Arg.Any<string>())
.Returns(subscription);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
@@ -1765,4 +1762,142 @@ public class SubscriberServiceTests
}
#endregion
#region IsValidGatewayCustomerIdAsync
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.IsValidGatewayCustomerIdAsync(null));
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_NullGatewayCustomerId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = null;
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_EmptyGatewayCustomerId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = "";
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_ValidCustomerId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId).Returns(new Customer());
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await stripeAdapter.Received(1).CustomerGetAsync(organization.GatewayCustomerId);
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_InvalidCustomerId_ReturnsFalse(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } };
stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId).Throws(stripeException);
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.False(result);
await stripeAdapter.Received(1).CustomerGetAsync(organization.GatewayCustomerId);
}
#endregion
#region IsValidGatewaySubscriptionIdAsync
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(null));
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_NullGatewaySubscriptionId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = null;
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.SubscriptionGetAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_EmptyGatewaySubscriptionId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = "";
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.SubscriptionGetAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_ValidSubscriptionId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId).Returns(new Subscription());
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await stripeAdapter.Received(1).SubscriptionGetAsync(organization.GatewaySubscriptionId);
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_InvalidSubscriptionId_ReturnsFalse(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } };
stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId).Throws(stripeException);
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.False(result);
await stripeAdapter.Received(1).SubscriptionGetAsync(organization.GatewaySubscriptionId);
}
#endregion
}

View File

@@ -2,10 +2,13 @@
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.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@@ -19,17 +22,93 @@ public class HandlebarsMailServiceTests
private readonly GlobalSettings _globalSettings;
private readonly IMailDeliveryService _mailDeliveryService;
private readonly IMailEnqueuingService _mailEnqueuingService;
private readonly IDistributedCache _distributedCache;
public HandlebarsMailServiceTests()
{
_globalSettings = new GlobalSettings();
_mailDeliveryService = Substitute.For<IMailDeliveryService>();
_mailEnqueuingService = Substitute.For<IMailEnqueuingService>();
_distributedCache = Substitute.For<IDistributedCache>();
_sut = new HandlebarsMailService(
_globalSettings,
_mailDeliveryService,
_mailEnqueuingService
_mailEnqueuingService,
_distributedCache
);
}
[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>()
);
}
@@ -137,8 +216,9 @@ public class HandlebarsMailServiceTests
};
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
var distributedCache = Substitute.For<IDistributedCache>();
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService());
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache);
var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync");

View File

@@ -9,7 +9,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using IdentityModel;
using Duende.IdentityModel;
using Microsoft.AspNetCore.DataProtection;
using Xunit;